
//
// 120314 - AH - (Aura *) Created.
// 120318a- AH - respond(HttpServletResponse, SoekratDefaultResponse).
// 120318b- AH - RequestSupport introduced.
// 120319a- AH - printAsText(HttpServletResponse, byte[]).
// 120319b- AH - respond(HttpServletResponse, Object) and 
//               respond(HttpServletResponse, String).
// 120320 - AH - printHtmlLink(PrintWriter, String, String, String) and
//               printAsText(HttpServletResponse res, byte[] bytes).
// 120325 - AH - getParameterMap(HttpServletRequest, String...).
// 120331 - AH - parseParameterString(String).
// 120401 - AH - Renamed getMandatoryReqParameterAsNonNegLong from
//               getReqParameterAsNonNegLong.
// 120411 - AH - (Huset) MetaParameter, addMetaParameter(MetaParameter) and
//               genereateFormHtml(String, boolean).
// 120504 - AH - getHttpRequestDef().
// 120518 - AH - (Huset *) Renamed from AbstractSokratHttpServlet. spelling 
//               error that 'e' missed in Soekrat.
//

package org.soekrat.server.servlets;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.japril.servlet.HttpRequestDef;
import org.japril.servlet.HttpRequestParameter;
import org.soekrat.environment.OrbSuppliers;
import org.soekrat.server.orbsuppliers.CaptorSupplier;
import org.soekrat.server.orbsuppliers.ConversationElementSupplier;
import org.soekrat.server.orbsuppliers.ServerSoekratDatabaseOrbSupplier;
import org.soekrat.server.orbsuppliers.s2.S2OrbSupplier;
import org.soekrat.server.serialization.TextSerializator;

/**
 * <p>
 *   Base class for Soekrat
 *   {@link HttpServlet}s.
 * </p>
 * <p>
 *   Declare an abstract method to handle request both as GET and POST but
 *   with the ability for implementations to tell only GET or only POST should
 *   be allowed.
 * </p>
 * <p>
 *   Has utility methods to ease servlet programming and testing.
 * </p>
 * @author Arne Halvorsen (AH)
 * @author Laetitia Le Chatton (LLC)
 */
@SuppressWarnings("serial")
public abstract class AbstractSoekratHttpServlet extends HttpServlet 
{
  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public AbstractSoekratHttpServlet(){}

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException 
  {
    if (supportGet())
    {  
      handleRequest(req, res);
    }
    else
    {
      respond(res, new SoekratResponse("GET not suppoerted"));
    }
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException 
  {
    if (supportPost())
    {  
      handleRequest(req, res);
    }
    else
    {
      respond(res, new SoekratResponse("POST not suppoerted"));
    }
  }
  
  /**
   * <p>
   *   Tells if support GET (default us true).
   * </p>
   * @return {@code true} if does, {@code false} else.
   */
  protected boolean supportGet(){ return true; }
  
  /**
   * <p>
   *   Tells if support POST (default us true).
   * </p>
   * @return {@code true} if does, {@code false} else.
   */
  protected boolean supportPost(){ return true; }

  /**
   * <p>
   *   Invoked from 
   *   {@link #doGet(HttpServletRequest, HttpServletResponse)} and
   *   {@link #doPost(HttpServletRequest, HttpServletResponse)} and 
   *   implementation overrides to handle request.
   * </p>
   * @param req {@link HttpServletRequest}.
   * @param res {@link HttpServletResponse}.
   */
  protected abstract void handleRequest(HttpServletRequest req, 
    HttpServletResponse res) throws ServletException, IOException;
  
  /**
   * <p>
   *   Tells if request has parameters.
   * </p>
   * @param req Request.
   * @return {@code true} if has, {@code false} if not.
   */
  protected static boolean hasParameters(HttpServletRequest req)
  {
    if (req==null)
    {
      throw new NullPointerException("req");
    }
    
    @SuppressWarnings("rawtypes")
    Map map=req.getParameterMap();
    return map==null ? false : map.size()>0;
  }
  
  /**
   * <p>
   *   Convenient method for starting writing plain HTML response.
   * </p>
   * @param res {@link HttpServletResponse}.
   * @return {@link PrintWriter} to write content HTML to.
   * @throws IOException If fails.
   */
  protected static PrintWriter printSimpleHtmlStart(HttpServletResponse res)
    throws IOException
  {
    return printSimpleHtmlStart(res, null);  
  }
  
  /**
   * <p>
   *   Convenient method for starting writing plain HTML response.
   * </p>
   * @param res   {@link HttpServletResponse}.
   * @param title Title of page, can be {@code null}.
   * @return {@link PrintWriter} to write content HTML to.
   * @throws IOException If fails.
   */
  protected static PrintWriter printSimpleHtmlStart(HttpServletResponse res, 
    String title) throws IOException
  {
    res.setContentType("text/html");
    
    PrintWriter pw=res.getWriter();
    pw.println("<html><body>");
    if (title!=null) 
    {
      pw.println("<h1>"+title+"</h1>");
    }
    
    return pw;
  }
  
  /**
   * <p>
   *   Convenient method for writing final HTML when writing plain HTML 
   *   response.
   * </p>
   * @param pw {@link PrintWriter} to write HTML to.
   */
  protected static void printSimpleHtmlEnd(PrintWriter pw)
  {
    if (pw==null)
    {
      throw new NullPointerException("pw");
    }
    
    pw.println("</body></html>");
    pw.flush();
  }
  
  /**
   * <p>
   *   Utility method to print HTML link.
   * </p>
   * @param pw   Stream to print to.
   * @param link URL.
   * @param desc Text to be link, if {@code null} compiled URL text is used.
   */
  protected static void printHtmlLink(PrintWriter pw, String link, String desc)
  {
    printHtmlLink(pw, null, link, desc);
  }
  
  /**
   * <p>
   *   Utility method to print HTML link.
   * </p>
   * @param pw   Stream to print to.
   * @param stem First part of URL, may be {@code null} if {@code link} is not.
   * @param link Last part of URL, may be {@code null} if {@code stem} is not.
   * @param desc Text to be link, if {@code null} compiled URL text is used.
   */
  protected static void printHtmlLink(PrintWriter pw, String stem, String link, 
    String desc)
  {
    if (pw==null)
    {
      throw new NullPointerException("pw");
    }
    if (stem==null && link==null)
    {
      throw new NullPointerException("stem && link");
    }
    
    StringBuilder sb=new StringBuilder();
    
    if (stem!=null) sb.append(stem).append('/');
    if (link!=null) sb.append(link);
    String url=sb.toString();
    
    desc=(desc==null) ? url : desc;
    
    sb.setLength(0);
    sb.append("<a href='").append(url).append("'>").append(desc)
      .append("</a>").append("<br/>");
    
    pw.println(sb);
  }
  
  /**
   * <p>
   *   Convenient method when responding successfully with data to be serialized 
   *   back to client. 
   * </p>
   * @param res {@code HttpServletResponse}.
   * @param s   Object to be serialized and sent to client.
   * @throws IOException If fails.
   */
  protected static void respond(HttpServletResponse res, Object s)
    throws IOException
  {
    if (res==null)
    {
      throw new NullPointerException("res");
    }
    if (s==null)
    {
      throw new NullPointerException("s");
    }
    
    respond(res, new SoekratResponse(s));
  }
  
  /**
   * <p>
   *   Convenient method when only response is the status message.
   * </p>
   * <p>
   *   Most likely used for error responses but also if a simple "ok" is enough.
   * </p>
   * @param res    {@code HttpServletResponse}.
   * @param status Status message.
   * @throws IOException If fails.
   * @throws IllegalArgumentException If {@code status} is the empty string.
   */
  protected static void respond(HttpServletResponse res, String status)
    throws IOException
  {
    if (res==null)
    {
      throw new NullPointerException("res");
    }
    if (status==null)
    {
      throw new NullPointerException("status");
    }
    
    status=status.trim();
    if (status.length()==0)
    {
      throw new IllegalArgumentException("status the empty string");
    }
    
    respond(res, new SoekratResponse(status));
  }
  
  /**
   * <p>
   *   Convenient method to write a
   *   {@link SoekratResponse} back to client.
   * </p>
   * @param res {@code HttpServletResponse}.
   * @param r   {@code SoekratDefaultResponse}-
   * @throws IOException If fails.
   */
  protected static void respond(HttpServletResponse res, 
    SoekratResponse r) throws IOException
  {
    if (res==null)
    {
      throw new NullPointerException("res");
    }
    if (r==null)
    {
      throw new NullPointerException("r");
    }
    
    TextSerializator ts=TextSerializator.getTextSerializator();
    res.setContentType(ts.getMime());
    ts.print(res.getWriter(), r);
  }
  
  /**
   * <p>
   *   Validates a
   *   {@link HttpServletRequest request} given a request
   *   {@link HttpRequestDef definition} and if is not accepted responds with
   *   the error message validation produced.
   * </p>
   * @param res {@link HttpServletResponse}.
   * @param def Request definition.
   * @param req {@link HttpServletRequest}.
   * @return {@code true} if validated, {@code false} an error message has
   *         been sent to the client and the servlet should terminate.
   * @throws IOException If failed to respond error message to client.
   */
  protected static boolean validate(HttpServletResponse res, HttpRequestDef def, 
    HttpServletRequest req) throws IOException
  {
    if (res==null)
    {
      throw new NullPointerException("res");
    }
    if (def==null)
    {
      throw new NullPointerException("def");
    }
    if (req==null)
    {
      throw new NullPointerException("req");
    }
    
    String error=def.validate(req);
    if (error==null) return true;
    respond(res, error);
    return false;
  }
  
  /**
   * <p>
   *   Parse given string as non negative long.
   * </p>
   * @param name Parameter name. 
   * @param s    Parameter string value.
   * @param res  {@link HttpServletResponse}.
   * @return Parsed value or {@code null} if not accepted and error message
   *         sent back to client.
   * @throws IOException If fail to send error message to client.
   */
  protected Long getMandatoryStringAsNonNegLong(String name, String s, 
    HttpServletResponse res) throws IOException
  {
    if (s==null)
    {
      respond(res, "Parameter: "+name+" not found");
      return null;
    }
    
    try
    {
      long v=Long.parseLong(s);
      if (v<0)
      {
        respond(res, "Parameter: "+name+"<0 : "+v);
        return null;
      }
      
      return v;
    }
    catch (NumberFormatException nfx)
    {
      respond(res, "Parameter: "+name+" could not be parsed as long: "+s);
      return null;
    }
  }
  
  /**
   * <p>
   *   Alternative to 
   *   {@link HttpServletRequest#getParameterMap()} to exclude parameters to be
   *   put in map.
   * </p>
   * @param req    Request to get parameters from.
   * @param ignore Names on parameters that should be ignored. If {@code null}
   *               none is ignored.
   * @return Map where keys are parameter names and values are parameter values.
   */
  protected static Map<String, String> getParameterMap(HttpServletRequest req, 
    String... ignore)
  {
    if (req==null)
    {
      throw new NullPointerException("req");
    }
    
    Map<String, String> retVal=new HashMap<String, String>();
    
    @SuppressWarnings("rawtypes")
    Enumeration pnames=req.getParameterNames();
    while (pnames.hasMoreElements())
    {
      Object curr=pnames.nextElement();
      if (curr!=null)
      {
        String name=curr.toString();
        if (!contains(name, ignore))
        {
          String value=req.getParameter(name);
          retVal.put(name, value);
        }
      }
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Parse a string giving parameters in the format 
   *   "name1=val1,name2=val2,...".
   * </p>
   * <p>
   *   Simply ignores what can not be parsed.
   * </p>
   * @param s String to parse, {@code null} is treated as the empty string: a
   *          empty map.          
   * @return Map where keys are the parsed parameter names and values are parsed
   *         parameter values.
   */
  protected static Map<String, String> parseParameterString(String s)
  {
    s=(s==null) ? "" : s.trim();
    
    Map<String, String> retVal=new HashMap<String, String>();
    
    StringTokenizer st=new StringTokenizer(s, ",");
    while (st.hasMoreTokens())
    {
      String curr=st.nextToken();
      int idx=curr.indexOf('=');
      if (idx!=-1)
      {
        String name=curr.substring(0, idx).trim();
        if (name.length()>0)
        {
          String value=curr.substring(idx+1).trim();
          if (value.length()>0)
          {
            retVal.put(name, value);
          }
        }
      }
    }
    
    return retVal;
  }
  
  private static boolean contains(String name, String[] names)
  {
    if (names==null) return false;
    for (String curr : names) if (name.equals(curr)) return true;  
    return false;
  }
  
  /**
   * <p>
   *   Reads stream and deliver content as an array of bytes.
   * </p>
   * @param is Stream to read.
   * @return Content.
   * @throws IOException If fails.
   */
  protected static byte[] readBytes(InputStream is) throws IOException
  {
    ByteArrayOutputStream baos=new ByteArrayOutputStream();
    
    byte[] buf=new byte[1024];
    int n;
    while ((n=(is.read(buf)))!=-1) baos.write(buf, 0, n);  
    baos.flush();
    return baos.toByteArray();
  }
  
  /**
   * <p>
   *   Gets 
   *   {@link HttpRequestDef} that described
   *   {@code this}.
   * </p>
   * <p>
   *   Not recommended to <b>not</b> override.
   * </p>
   * @return {@link HttpRequestDef} or {@code null} if has no description.
   */
  public HttpRequestDef getHttpRequestDef(){ return null; }
  
  /**
   * <p>
   *   Gets
   *   {@link OrbSuppliers} for this (the server) conversation environment.
   * </p>
   * @return Created.
   */
  protected OrbSuppliers getOrbSuppliers()
  {
    OrbSuppliers retVal=new OrbSuppliers();
    
    retVal.addSupplier(new ConversationElementSupplier());
    
    retVal.addSupplier(new ServerSoekratDatabaseOrbSupplier());
    
    S2OrbSupplier.soekrat2Initializers(retVal);
    
    retVal.addSupplier(new CaptorSupplier());
    
    System.err.println("orbs suppliers: "+retVal);
    
    return retVal;
  }
  
  /**
   * <p>
   *   Request parameter description for visitor's name.
   * </p>
   */
  protected static final HttpRequestParameter VISITOR_NAME=
    new HttpRequestParameter("name").desc("Visitor");
  
  /**
   * <p>
   *   Request parameter description for conversation element id.
   * </p>
   */
  protected static final HttpRequestParameter CE_ID=
    new HttpRequestParameter("ce-id").desc("Conversation element Id")
      .clazz(Long.class);
  
  /**
   * <p>
   *   Request parameter description for account id.
   * </p>
   */
  protected static final HttpRequestParameter ACCOUNT_ID=
    new HttpRequestParameter("account-id").desc("Account holder's id")
      .clazz(Long.class);
  
  /**
   * <p>
   *   Request parameter description for new name for account holder's name. 
   * </p>
   */
  protected static final HttpRequestParameter ACCOUNT_NEW_NAME=
    new HttpRequestParameter("new-account-name")
      .desc("New name on account holder");
  
  /**
   * <p>
   *   Request parameter description for name for conversation element's name. 
   * </p>
   */
  protected static final HttpRequestParameter CE_NAME=
    new HttpRequestParameter("ce-name").desc("Name");
 
  /**
   * <p>
   *   Request parameter description for name on engine to use handling an
   *   request which need to interact with one.
   * </p>
   */
  protected static final HttpRequestParameter ENGINE=
    new HttpRequestParameter("engine").ftype("select").option("soekrat2");
 
  /**
   * <p>
   *   Request parameter description for folder id for a rat database. 
   * </p>
   */
  protected static final HttpRequestParameter RAT_BASE_ID=
    new HttpRequestParameter("rat-base-id").desc("Rat Base id")
      .clazz(Long.class);
  
  /**
   * <p>
   *   Request parameter description for name of semantic effect name. 
   * </p>
   */
  protected static final HttpRequestParameter SEMANTIC_EFFECT_NAME=
    new HttpRequestParameter("semantic-effect").desc("Name");
  
  /**
   * <p>
   *   Request parameter description for name of semantic effect action. 
   * </p>
   */
  protected static final HttpRequestParameter SEMANTIC_EFFECT_ACTION=
    new HttpRequestParameter("semantic-actiom").desc("Action");
  
  /**
   * <p>
   *   Request parameter description for chat input. 
   * </p>
   */
  protected static final HttpRequestParameter CHAT=
    new HttpRequestParameter("chat").ftype("textarea").cols(30).rows(5)
      .mandatory(false);
  
  /**
   * <p>
   *   Request parameter description for name of a folder.
   * </p>
   */
  protected static final HttpRequestParameter FOLDER_NAME=
    new HttpRequestParameter("folder-name").desc("Name");
  
  /**
   * <p>
   *   Request parameter description for name a folder's parent's id (not
   *   mandatory).
   * </p>
   */
  protected static final HttpRequestParameter PARENT_FOLDER_ID=
    new HttpRequestParameter("parent-folder-id").desc("Parent Folder Id")
      .clazz(Long.class).mandatory(false);
  
  /**
   * <p>
   *   Request parameter description for a folder's id.
   * </p>
   */
  protected static final HttpRequestParameter FOLDER_ID=
    new HttpRequestParameter("folder-id").desc("Folder Id");
  
  /**
   * <p>
   *   Request parameter description for a folder's parameters (not mandatory).
   * </p>
   */
  protected static final HttpRequestParameter FOLDER_PARAMETERS=
    new HttpRequestParameter("folder-parameters").desc("Folder Parameters")
      .mandatory(false);
  
  /**
   * <p>
   *   Request parameter description for the name of a file to upload. 
   * </p>
   */
  protected static final HttpRequestParameter UPLOAD_FILE_NAME=
    new HttpRequestParameter("upload-file-name").size(30);
  
  /**
   * <p>
   *   Request parameter description for the use of a file.
   * </p>
   */
  protected static final HttpRequestParameter FILE_USE=
    new HttpRequestParameter("file-use").desc("Use");
  
  /**
   * <p>
   *   Request parameter description for the type of a file.
   * </p>
   */
  protected static final HttpRequestParameter FILE_TYPE=
    new HttpRequestParameter("file-type").desc("Type");
  
  /**
   * <p>
   *   Request parameter description for telling a file type is pure text.
   * </p>
   */
  protected static final HttpRequestParameter TEXT_FILE=
    new HttpRequestParameter("text-file").desc("Pure text")
      .ftype("checkbox").fvalue("text");
  
  /**
   * <p>
   *   Request parameter description for a file to upload.
   * </p>
   */
  protected static final HttpRequestParameter FILE=
    new HttpRequestParameter("file").desc("File").ftype("file").size(40);
  
  /**
   * <p>
   *   Request parameter description for a file's id.
   * </p>
   */
  protected static final HttpRequestParameter FILE_ID=
    new HttpRequestParameter("file-id").desc("File Id").clazz(Long.class);
  
  /**
   * <p>
   *   Request parameter description for name of a rat data base.
   * </p>
   */
  protected static final HttpRequestParameter RAT_BASE_NAME=
    new HttpRequestParameter("name").desc("Name").size(30);
  
  /**
   * <p>
   *   Request parameter description for name of a wheel XML file (soekrat2).
   * </p>
   */
  protected static final HttpRequestParameter SOEKRAT=
    new HttpRequestParameter("soekrat").desc("File").size(40).ftype("file");
  
  /**
   * <p>
   *   Request parameter description for name of a spare XML file (soekrat2).
   * </p>
   */
  protected static final HttpRequestParameter SPARE=
    new HttpRequestParameter("spare").desc("File").size(40).ftype("file");
  
  /**
   * <p>
   *   Request parameter description for request having parameters coded in
   *   Json.
   * </p>
   */
  protected static final HttpRequestParameter JSON_REQ=
    new HttpRequestParameter("json").desc("Json Request Parameter")
    .ftype("textarea").cols(50).rows(30);
      
  
}
